Skip to content

feat(terminal): an input line on phones, instead of typing into the grid - #76

Merged
devswha merged 7 commits into
mainfrom
feat/terminal-input-line
Sep 26, 2026
Merged

devswha merged 7 commits into
mainfrom
feat/terminal-input-line

Conversation

@devswha

@devswha devswha commented Sep 26, 2026

Copy link
Copy Markdown
Owner

Stacked on #74 (← #73 ← #72 ← #71 ← #70). Merge in order. It merges cleanly with #75.

Bug

On a phone, text typed in the terminal lens came out repeated. The user's own report showed it: "널에 추가로 폰에서는채팅을 모바일 가터미널에 미널에…".

A phone keyboard rewrites what it typed: dictation revises a phrase, an IME composes a Korean syllable, autocorrect replaces a word. xterm sends each revision as new keys, and a terminal cannot take back keys it has already received.

What

  • Input line (touch screens only, pointer: coarse). TerminalInput sits above the key bar.
    • The line grows to four rows. Enter or the button sends it, and Shift+Enter breaks the line.
    • An IME still composing keeps its Enter.
    • While the line is empty, the button reads Enter and presses Enter alone, for a menu default or "press Enter to continue".
    • The font is ≥16px, so iOS does not zoom.
  • Sending "like the keyboard". The line goes as a submit with the new typed: true. The server skips agent.prompt, which refuses while an agent waits, and types the payload with send_text, then Enter after the usual SUBMIT_DELAY_MS gap. In the terminal, an answer typed into an open menu is what the user means.
    • One line goes as-is; several lines go as one bracketed paste when the program has it on.
    • Older servers ignore typed and behave as before.
  • The grid no longer raises the keyboard. xterm's hidden textarea gets inputmode="none", and key-bar taps leave focus in the line.
  • ⌨ direct-typing toggle. A key at the end of the key bar switches to typing straight into the grid (passwords, vim), remembered per device in herdr-web-ui:direct-typing.
  • Desktops are unchanged.

Tests

  • bun run typecheck; bun test 426/426.
  • New contract test (submit.contract.test.ts): a typed submit to an agent reported blocked types 2 then \r, with the Enter's gap kept. The existing test that an untyped submit is refused still passes.
  • test:ui 10/10, with a new touch scenario:
    • the input line shows and the grid textarea has inputmode=none
    • Enter sends a typed submit to the right pane, and the line clears on confirmation
    • the empty line's button sends \r
    • ⌨ removes the line and restores the textarea
    • no horizontal overflow
  • Screenshotted at 390 px: echo 안녕하세요 모바일 입력 arrived whole and ran.

Not verified

A real phone keyboard, i.e. Gboard/iOS dictation and the Korean IME. The headless touch emulation proves the paths, not the keyboards.

🤖 Generated with Claude Code

devswha and others added 7 commits September 27, 2026 00:23
One-tap messages (continue, yes, no, commit and push, retry to start with),
as collie's Quick dock has. A reply goes through the composer's own send
path, so it is queued while the agent works and taken as the answer when a
question card is open; a draft in the box is left alone.

The list lives in each device's settings: quick replies are a phone thing
(a PC has a keyboard), so nothing is stored on the server. Settings ->
Quick replies edits it (up to 12, 200 characters each; blank rows are kept
while being typed and never shown), and a button beside the paperclip
hides or shows the row, remembered per device.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The row takes a line above the message box on every pane; it now waits for
the ⚡ button, and the choice is remembered per device as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Transcripts already record what each request filled:
- Codex: token_count events with last_token_usage and model_context_window
- Claude Code: assistant usage, input + cache creation + cache read,
  subagent (sidechain) entries skipped
- omp/omo/gjc: message usage, input + cacheRead + cacheWrite

ConversationMetadata gains an optional `context { used, window }`. The
window is only what the transcript leaves no doubt about: Codex states it,
and a Claude request past 200k can only have run in the 1M window (kept
through a compaction). Otherwise the line shows the tokens used, never a
guessed percentage.

The composer shows `74% left` or `68k used`, red at 20% or less, with the
counts on hover. PaneTerminal's metadata dedupe compared only model and
effort, so it now compares the context too, or the number would never move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, a loading state

From comparing the chat with chatmux's:

- A failed tool call looked like a success inside "Worked for…". Tool parts
  gain `error`: Claude's tool_result is_error and omp's toolResult isError,
  and for Codex, which records no flag, the output's own verdict
  (exit code, "Script failed", "apply_patch verification failed"; a
  completed script is judged as a whole). The row reads "failed" in red,
  its output is headed Error, and the block header counts failures.
- A file path in a tool row opens the FileViewer, like paths in prose.
- Codex's apply_patch, bare or as the string an exec script passes to
  tools.apply_patch, renders as a per-file diff and its row is summed up
  by the files it touches (shared/patch.ts, used by server and client).
- The jump-to-latest button showed only for new messages; it now shows
  whenever the reader is scrolled up (a round arrow), and stays the "New
  messages" pill when there are some.
- The chat said "No conversation yet" until the first answer came back;
  it now says it is loading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
More from comparing with chatmux's chat:

- Images the user sent: Claude transcripts keep pasted images as base64
  in the user entry. The parser now emits an image part naming it
  (`<entry uuid>:<block index>`, png/jpeg/gif/webp only) instead of the
  data, and GET /api/pane/conversation/image decodes it on request
  (immutable cache), so a 2 s poll never carries megabytes. Images this
  app attaches appear as `@…png` mentions and show through /api/fs/file.
  Thumbnails sit above the user bubble; a tap opens them.
- Edit/MultiEdit render as one line diff (src/lib/diff.ts, an LCS that
  falls back to removed-then-added past 250k line pairs).
- A compaction's summary, which was dropped, is a divider that opens to
  it.
- Markdown headings #### to ###### render as h4-h6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… menu

- Tool outputs are cut at 4000 characters in the page. A cut one now
  keeps its call id and full length (output_ref, output_size; the cut
  moved into server/tool-output.ts, shared by the Claude, omp and Codex
  parsers). GET /api/pane/conversation/tool-output finds the call's result
  in the transcript and returns it as text (up to 2 MB, immutable cache);
  the row offers "Show the whole output (48k characters)" and shows it
  in a box that scrolls on its own.
- The `/` menu for Claude adds user and project skills (SKILL.md) and the
  skills and commands of plugins enabled in ~/.claude/settings.json, as
  /<plugin>:<name>. For Codex it adds saved prompts (/prompts:<name>) and
  its skills, which Codex takes as `$name`: a `$` where a word starts opens
  them, only in a Codex pane.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a phone, text typed in the terminal lens came out repeated ("널에
추가로 폰에서는채팅을 모바일 가터미널에 미널에…"): the keyboard rewrites
what it typed (dictation revising a phrase, an IME composing a Korean
syllable, autocorrect), and xterm sends each revision as new keys a
terminal cannot take back. Termius documents the same problem; Moshi and
Termius both answer it with a panel where the text is finished first and
sent whole.

- On a coarse pointer, the terminal lens gets TerminalInput above the key
  bar: a line that grows to four, Enter or the button sends it, an empty
  line's button presses Enter alone. It is written with the keyboard's own
  editing and sent whole.
- It goes as a submit with `typed`: the server types it like the keyboard
  (send_text, then Enter after the usual gap) instead of agent.prompt,
  which refuses while an agent waits, since in the terminal an answer to
  an open menu is what the user means. Several lines go as one paste.
- The grid's hidden textarea gets inputmode=none, so a tap no longer raises
  the keyboard, and key-bar taps leave focus in the line. A ⌨ key on the
  key bar switches to typing straight into the grid, remembered per device.
- Desktops (fine pointer) are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant